package se.fk.klistret.console;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.PosixParser;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.Properties;

import javax.ws.rs.core.MediaType;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.stream.StreamSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;

import org.jboss.resteasy.client.ClientRequest;
import org.jboss.resteasy.client.ClientResponse;
import org.jboss.resteasy.plugins.providers.jaxb.JaxbCollection;

import com.klistret.cmdb.ci.pojo.Element;
import com.klistret.cmdb.ci.commons.Property;
import com.klistret.cmdb.ci.element.component.Software;
import com.klistret.cmdb.ci.element.component.Publication;
import com.klistret.cmdb.ci.element.process.change.SoftwareInstallation;

import se.fk.klistret.console.exceptions.ApplicationException;
import se.fk.klistret.console.exceptions.InfrastructureException;
import se.fk.klistret.console.jaxb.CIContext;
import se.fk.klistret.console.ui.TextMenu;
import se.fk.klistret.console.ui.TextMenuEvent;
import se.fk.klistret.console.ui.TextMenuEventListener;
import se.fk.klistret.console.ui.TextMenuItem;

/**
 * 
 * @author Matthew Young
 * 
 */
public class TextBasedClient {
	private static final Logger logger = LoggerFactory
			.getLogger(TextBasedClient.class);

	/**
	 * Client properties
	 */
	private Properties clientProperties;

	/**
	 * Ant build file housing logic for running an installation
	 */
	private URL antBuildURL;

	/**
	 * Choosen software installation
	 */
	private Element softwareInstallation;

	/**
	 * Choosen software
	 */
	private Element software;

	/**
	 * Navigation expression for moving next,previous
	 */
	private final static String navigationExpression = "^(n|p)";

	/**
	 * Update expression for changing the state of the publication installation
	 */
	private final static String updateExpression = "^(u|update)";

	/**
	 * Environment properties
	 */
	private Properties environmentProperties;

	/**
	 * Organization properties
	 */
	private Properties organizationProperties;

	/**
	 * SCP locations are simple beans representing file stores on remote hosts
	 */
	private Hashtable<String, SCPLocation> scpLocations;

	/**
	 * JAXB Unmarshaller
	 */
	private Unmarshaller unmarshaller;

	/**
	 * Enhet (Or unit denotes a test level below the first test environment)
	 * flag set to false indicating that the sub-directory enhetstest is not
	 * added to the value of the tuxdocs.directory property
	 */
	private boolean enhet = false;

	/**
	 * Page Default limit
	 */
	private int pageDefaultLimit = 25;

	/**
	 * Client properties are loaded to establish which environment, CMDB
	 * instance, Ant build file and SCP settings should be used.
	 * 
	 * @param file
	 */
	private void loadClientProperties(File file) {
		clientProperties = new Properties();

		try {
			clientProperties.load(new FileInputStream(file));
		} catch (IOException e) {
			throw new InfrastructureException(String.format(
					"Client property file [%s] could not be loaded: %s",
					file.getPath(), e.getMessage()));
		}

		if (clientProperties.getProperty("cmdb.resteasy.url") == null)
			throw new ApplicationException(
					String.format(
							"Property [cmdb.resteasy.url] not found in client property file [%s]",
							file.getPath()));

		if (clientProperties.getProperty("environment.name") == null)
			throw new ApplicationException(
					String.format(
							"Property [environment.name] not found in client property file [%s]",
							file.getPath()));

		if (clientProperties.getProperty("organization.name") == null)
			throw new ApplicationException(
					String.format(
							"Property [organization.name] not found in client property file [%s]",
							file.getPath()));

		if (clientProperties.getProperty("ant.build.xml.url") == null)
			throw new ApplicationException(
					String.format(
							"Property [ant.build.xml.url] not found in client property file [%s]",
							file.getPath()));

		if (clientProperties.getProperty("paging.limit") != null) {
			pageDefaultLimit = Integer.parseInt(clientProperties
					.getProperty("paging.limit"));

			if (pageDefaultLimit > 50) {
				System.out.println(String.format(
						"Paging limit %d changed to max 50", pageDefaultLimit));
				pageDefaultLimit = 50;
			}

			if (pageDefaultLimit < 10) {
				System.out.println(String.format(
						"Paging limit %d less than 10, change to min 10",
						pageDefaultLimit));
				pageDefaultLimit = 10;
			}
		}

		try {
			antBuildURL = new URL(clientProperties.getProperty(
					"ant.build.xml.url").trim());
		} catch (MalformedURLException e) {
			throw new InfrastructureException(String.format(
					"MalformedURLException building url to ant build file: ",
					e.getMessage()));
		}

		scpLocations = new Hashtable<String, SCPLocation>(
				clientProperties.size());

		for (Enumeration<?> keys = clientProperties.propertyNames(); keys
				.hasMoreElements();) {
			String key = (String) keys.nextElement();

			try {
				if (key.matches("scp\\.tarfil\\.\\w*\\.(pattern|directory)")) {
					String[] keyLabels = key.split("\\.");

					SCPLocation scpLocation = null;
					if (!scpLocations.containsKey(keyLabels[2])) {
						scpLocation = new SCPLocation();
					} else {
						scpLocation = (SCPLocation) (scpLocations
								.get(keyLabels[2]));
					}

					if (keyLabels[3].equals("pattern")) {
						scpLocation.setPattern(clientProperties
								.getProperty(key));
					}
					if (keyLabels[3].equals("directory")) {
						scpLocation.setDirectory(clientProperties
								.getProperty(key));
					}

					scpLocations.put(keyLabels[2], scpLocation);
				}
			} catch (java.util.regex.PatternSyntaxException e) {
				throw new InfrastructureException(String.format(
						"Syntax error with scp properties: %s", e.getMessage()));
			}
		}
	}

	/**
	 * Finds the location of the remote directory based on the software name
	 * 
	 * @param name
	 * @return
	 */
	private SCPLocation findScpLocation(String name) {
		for (Enumeration<SCPLocation> locations = scpLocations.elements(); locations
				.hasMoreElements();) {
			SCPLocation scpLocation = (SCPLocation) locations.nextElement();
			String scpPattern = scpLocation.getPattern();

			try {
				if (name.matches(scpPattern))
					return scpLocation;
			} catch (java.util.regex.PatternSyntaxException e) {
				throw new InfrastructureException(String.format(
						"Syntax error with scp pattern [%s]: ", scpPattern,
						e.getMessage()));
			}
		}

		return null;
	}

	/**
	 * Count element based on expression filters
	 * 
	 * @param expressions
	 * @return
	 */
	private int countElements(String[] expressions) {
		String url = String.format("%s/element/count",
				clientProperties.getProperty("cmdb.resteasy.url"));

		try {
			for (int index = 0; index < expressions.length; index++) {
				url = url.concat(String.format("%sexpressions=%s",
						index == 0 ? "?" : "&",
						URLEncoder.encode(expressions[index], "UTF-8")));
			}

			ClientRequest request = new ClientRequest(url);
			request.accept("application/xml");
			ClientResponse<String> response = request.get(String.class);

			String entity = response.getEntity();

			if (response.getStatus() == 200)
				return Integer.parseInt(entity);

			throw new InfrastructureException(String.format(
					"HTTP client response [%s]", response.getStatus()));
		} catch (UnsupportedEncodingException e) {
			throw new InfrastructureException(String.format(
					"Encoding exception while calling Rest service: %s",
					e.getMessage()));
		} catch (JAXBException e) {
			throw new InfrastructureException(String.format(
					"Unmarshalling exception while calling Rest service: %s",
					e.getMessage()));
		} catch (Exception e) {
			throw new InfrastructureException(String.format(
					"Unknown exception while calling Rest service: %s",
					e.getMessage()));
		}
	}

	/**
	 * Find elements with expression filters
	 * 
	 * @param expressions
	 * @return
	 */
	private List<Element> findElements(String[] expressions, int start,
			int limit) {
		String url = String.format("%s/element",
				clientProperties.getProperty("cmdb.resteasy.url"));

		try {
			for (int index = 0; index < expressions.length; index++) {
				url = url.concat(String.format("%sexpressions=%s",
						index == 0 ? "?start=" + start + "&limit=" + limit
								+ "&" : "&",
						URLEncoder.encode(expressions[index], "UTF-8")));
			}

			ClientRequest request = new ClientRequest(url);
			request.accept("application/xml");
			ClientResponse<String> response = request.get(String.class);

			List<Element> results = new ArrayList<Element>();

			if (response.getStatus() == 200) {
				StreamSource source = new StreamSource(new StringReader(
						response.getEntity()));

				JAXBElement<JaxbCollection> collection = unmarshaller
						.unmarshal(source, JaxbCollection.class);

				List<Object> values = collection.getValue().getValue();
				for (Object value : values)
					results.add((Element) unmarshaller.unmarshal((Node) value));

				return results;
			}

			throw new InfrastructureException(String.format(
					"HTTP client response [%s]", response.getStatus()));
		} catch (UnsupportedEncodingException e) {
			throw new InfrastructureException(String.format(
					"Encoding exception while calling Rest service: %s",
					e.getMessage()));
		} catch (JAXBException e) {
			throw new InfrastructureException(String.format(
					"Unmarshalling exception while calling Rest service: %s",
					e.getMessage()));
		} catch (Exception e) {
			throw new InfrastructureException(String.format(
					"Unknown exception while calling Rest service: %s",
					e.getMessage()));
		}
	}

	/**
	 * Get an element
	 * 
	 * @param id
	 * @return
	 */
	private Element getElement(Long id) {
		String url = String.format("%s/element/%d",
				clientProperties.getProperty("cmdb.resteasy.url"), id);

		try {
			ClientRequest request = new ClientRequest(url);
			request.accept("application/xml");
			ClientResponse<String> response = request.get(String.class);

			if (response.getStatus() == 200) {
				StreamSource source = new StreamSource(new StringReader(
						response.getEntity()));

				return (Element) unmarshaller.unmarshal(source);
			}
		} catch (Exception e) {
			throw new InfrastructureException(String.format(
					"Unknown exception while calling Rest service: %s",
					e.getMessage()));
		}

		return null;
	}

	/**
	 * 
	 * @param expressions
	 * @return
	 */
	private Element getElement(String[] expressions) {
		String url = String.format("%s/element/unique",
				clientProperties.getProperty("cmdb.resteasy.url"));

		try {
			for (int index = 0; index < expressions.length; index++) {
				url = url.concat(String.format("%sexpressions=%s",
						index == 0 ? "?" : "&",
						URLEncoder.encode(expressions[index], "UTF-8")));
			}

			ClientRequest request = new ClientRequest(url);
			request.accept("application/xml");
			ClientResponse<String> response = request.get(String.class);

			if (response.getStatus() == 200) {
				StreamSource source = new StreamSource(new StringReader(
						response.getEntity()));

				Element element = (Element) unmarshaller.unmarshal(source);

				return element;
			}

			if (response.getStatus() == 400)
				return null;

			if (response.getStatus() == 204)
				return null;

			throw new InfrastructureException(String.format(
					"HTTP client response [%s]", response.getStatus()));
		} catch (UnsupportedEncodingException e) {
			throw new InfrastructureException(String.format(
					"Encoding exception while calling Rest service: %s",
					e.getMessage()));
		} catch (JAXBException e) {
			throw new InfrastructureException(String.format(
					"Unmarshalling exception while calling Rest service: %s",
					e.getMessage()));
		} catch (Exception e) {
			throw new InfrastructureException(String.format(
					"Unknown exception while calling Rest service: %s",
					e.getMessage()));
		}
	}

	/**
	 * Update element
	 * 
	 * @param element
	 * @return
	 */
	public Element updateElement(Element element) {
		String url = String.format("%s/element",
				clientProperties.getProperty("cmdb.resteasy.url"));

		try {
			ClientRequest request = new ClientRequest(url);
			request.accept("application/xml");
			request.body(MediaType.APPLICATION_XML, element);

			ClientResponse<String> response = request.put(String.class);

			if (response.getStatus() == 200) {
				StreamSource source = new StreamSource(new StringReader(
						response.getEntity()));

				logger.debug("Update of element [id: {}, name: {}] successful",
						element.getId(), element.getName());
				return (Element) unmarshaller.unmarshal(source);
			}
			throw new InfrastructureException(String.format(
					"HTTP client response [%s]", response.getStatus()));
		} catch (Exception e) {
			throw new InfrastructureException(String.format(
					"Unknown exception while calling Rest service: %s",
					e.getMessage()));
		}
	}

	/**
	 * 
	 * @return
	 */
	private String getSequenceValues(String line) {
		String text = null;

		String[] values = line.split(",");

		for (String value : values)
			text = text == null ? String.format("\"%s\"", value.trim())
					: String.format("%s,\"%s\"", text, value.trim());

		return text;
	}

	/**
	 * Menu for selecting software installations
	 * 
	 * @return
	 */
	private TextMenu getSelectionTextMenu() {
		final TextMenu menu = new TextMenu("Select software installation");

		menu.addActionListener(new TextMenuEventListener() {

			private int pageLimit = pageDefaultLimit;

			private int pageStart = 0;

			private Integer count = null;

			private String[] filters = null;

			private String[] getExpressions() {
				/**
				 * Create new expression filters
				 */
				List<String> expressions = new ArrayList<String>();

				/**
				 * Only active software installations
				 */
				expressions
						.add("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element[empty(pojo:toTimeStamp)][pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/process/change}SoftwareInstallation\"]");

				/**
				 * Filter by environment
				 */
				expressions.add(String
						.format("declare namespace change=\"http://www.klistret.com/cmdb/ci/element/process/change\"; declare namespace commons=\"http://www.klistret.com/cmdb/ci/commons\"; declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element/pojo:configuration/change:Environment[commons:Name eq \"%s\"]",
								clientProperties
										.getProperty("environment.name")));
				System.out.println(String.format(
						"Filtering by environment %s...",
						clientProperties.getProperty("environment.name")));
				logger.debug(
						"Adding software installation filter environment [name: {}]",
						clientProperties.getProperty("environment.name"));

				/**
				 * Filter out Software only
				 */
				expressions
						.add("declare namespace change=\"http://www.klistret.com/cmdb/ci/element/process/change\"; declare namespace process=\"http://www.klistret.com/cmdb/ci/element/process\"; declare namespace commons=\"http://www.klistret.com/cmdb/ci/commons\"; declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element/pojo:configuration[change:Software/commons:QName eq \"{http://www.klistret.com/cmdb/ci/element/component}Software\"]");

				/**
				 * Filter by states
				 */
				if (clientProperties.getProperty("installation.states") == null) {
					expressions
							.add("declare namespace process=\"http://www.klistret.com/cmdb/ci/element/process\"; declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element/pojo:configuration[process:State = (\"Planned\")]");
					System.out.println("Using default state Planned...");
					logger.debug("Adding default state filter [Planned]");
				} else {
					String text = getSequenceValues(clientProperties
							.getProperty("installation.states"));

					expressions.add(String
							.format("declare namespace process=\"http://www.klistret.com/cmdb/ci/element/process\"; declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element/pojo:configuration[process:State = (%s)]",
									text));
					System.out.println(String
							.format("Using states %s...", text));
					logger.debug("Adding state filtes [{}] from property file",
							text);
				}

				/**
				 * Filter by tags
				 */
				if (clientProperties.getProperty("installation.tags") != null) {
					String text = getSequenceValues(clientProperties
							.getProperty("installation.tags"));

					expressions.add(String
							.format("declare namespace commons=\"http://www.klistret.com/cmdb/ci/commons\"; declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element/pojo:configuration[commons:Tag = (%s)]",
									text));
					System.out.println(String.format("Using tags %s...", text));
					logger.debug("Adding tag filtes [{}] from property file",
							text);
				}

				return expressions.toArray(new String[0]);
			}

			private void count() {
				filters = getExpressions();
				count = countElements(filters);
				System.out.println(String.format(
						"Found %d software installations\n", count));
				logger.debug("Found {} software installations", count);
			}

			private TextMenuItem createTextMenuItem(Element element) {
				TextMenuItem item = new TextMenuItem();

				SoftwareInstallation configuration = ((SoftwareInstallation) element
						.getConfiguration());
				item.setLabel(configuration.getLabel());
				item.setId(element.getId());
				item.setEntity(element);

				item.addActionListener(new TextMenuEventListener() {

					public void actionPerformed(TextMenuEvent event) {
						TextMenuItem item = (TextMenuItem) event.getSource();

						softwareInstallation = (Element) item.getEntity();
						System.out.println(String.format(
								"%s is the active software installation",
								item.getLabel()));

						software = (Element) getElement(((SoftwareInstallation) softwareInstallation
								.getConfiguration()).getSoftware().getId());
						System.out.println(String.format(
								"%s is the active software to be installed",
								((Software) software.getConfiguration())
										.getLabel()));

						logger.debug(
								"Selected software installation [id: {}, name: {}]",
								softwareInstallation.getId(),
								softwareInstallation.getName());
					}

				});

				return item;
			}

			private void load() {
				menu.removeAll();

				List<Element> results = findElements(filters, pageStart,
						pageLimit);
				for (Element element : results) {
					menu.add(createTextMenuItem(element));
					logger.debug(
							"Added software installation [id: {}, name: {}] to menu",
							element.getId(), element.getName());
				}

				if (results.size() == 0) {
					System.out.println("No software installations found.");
					logger.warn("No softare installations found by filters");
				}
			}

			public void actionPerformed(TextMenuEvent event) {
				if (event.getIdentifier() == TextMenuEvent.ACTION_ITEM) {
					count();
					load();
				}

				if (event.getIdentifier() == TextMenuEvent.ACTION_EXPRESSION) {
					if (event.getCommand().matches(navigationExpression)) {
						String direction = event.getCommand();

						/**
						 * Page next
						 */
						if (direction.equals("n")) {
							if (pageDefaultLimit + pageStart < count) {
								pageStart = pageDefaultLimit + pageStart;
								System.out.println(String
										.format("Paging next [start: %d]...",
												pageStart));

								load();
							} else
								System.out.println("No more results");
						}

						/**
						 * Page previous
						 */
						if (direction.equals("p")) {
							if (pageStart >= pageDefaultLimit) {
								pageStart = pageStart - pageDefaultLimit;
								System.out.println(String.format(
										"Paging previous [start: %d] ...",
										pageStart));

								load();
							} else
								System.out.println("Results start.");
						}
					}
				}
			}
		});

		menu.setPrompt(" (Select) Enter name or item number > ");
		menu.setPromptExpression(navigationExpression);
		menu.setAutoExit(false);

		menu.setHelp("-- Command \"n\" pages to the next set of Software Installations.\n"
				+ "\n"
				+ "-- Command \"p\" pages to the previous set of Software Installations.\n");

		return menu;
	}

	/**
	 * Menu to perform installation of publications
	 * 
	 * @return
	 */
	private TextMenu getInstallationTextMenu() {
		final TextMenu menu = new TextMenu("Install a publication");

		menu.addActionListener(new TextMenuEventListener() {
			private boolean update = true;

			private TextMenuItem createTextMenuItem(Element element) {
				TextMenuItem item = new TextMenuItem();

				SoftwareInstallation configuration = ((SoftwareInstallation) element
						.getConfiguration());
				item.setLabel(configuration.getLabel());
				item.setId(element.getId());
				item.setEntity(element);

				item.addActionListener(new TextMenuEventListener() {

					public void actionPerformed(TextMenuEvent event) {
						TextMenuItem item = (TextMenuItem) event.getSource();

						System.out.println(String.format(
								"Installing publication %s...", item.getLabel()));
						Element element = (Element) item.getEntity();
						boolean success = install(element);

						if (success && update) {
							System.out
									.println("Setting publication installation to completed...");
							((SoftwareInstallation) element.getConfiguration())
									.setState("Completed");

							updateElement(element);
							menu.remove(item);

							logger.debug(
									"Removed menu item [id: {}, label: {}]",
									item.getId(), item.getLabel());

							if (menu.getItemCount() == 0) {
								System.out.println("Nothing more to install.");
								logger.debug("All publication installations completed");
							}
						}
					}
				});

				return item;
			}

			private String[] getExpressions(String commonsInstallationId) {
				/**
				 * Create new expression filters
				 */
				List<String> expressions = new ArrayList<String>();

				/**
				 * Only active software installations
				 */
				expressions
						.add("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element[empty(pojo:toTimeStamp)][pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/process/change}SoftwareInstallation\"]");

				/**
				 * Filter out Publication only
				 */
				expressions
						.add("declare namespace change=\"http://www.klistret.com/cmdb/ci/element/process/change\"; declare namespace process=\"http://www.klistret.com/cmdb/ci/element/process\"; declare namespace commons=\"http://www.klistret.com/cmdb/ci/commons\"; declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element/pojo:configuration[change:Software/commons:QName eq \"{http://www.klistret.com/cmdb/ci/element/component}Publication\"]");

				/**
				 * Filter out by common installation id
				 */
				expressions.add(String
						.format("declare namespace commons=\"http://www.klistret.com/cmdb/ci/commons\"; declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element/pojo:configuration/commons:Property[commons:Name eq \"common.installation.id\" and commons:Value eq \"%s\"]",
								commonsInstallationId));
				logger.debug(
						"Added publication installation filter common installation id [{}]",
						commonsInstallationId);
				
				/**
				 * Filter by states
				 */
				if (clientProperties.getProperty("installation.states") == null) {
					expressions
							.add("declare namespace process=\"http://www.klistret.com/cmdb/ci/element/process\"; declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element/pojo:configuration[process:State = (\"Planned\")]");
					System.out.println("Using default state Planned...");
					logger.debug("Adding default state filter [Planned]");
				} else {
					String text = getSequenceValues(clientProperties
							.getProperty("installation.states"));

					expressions.add(String
							.format("declare namespace process=\"http://www.klistret.com/cmdb/ci/element/process\"; declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element/pojo:configuration[process:State = (%s)]",
									text));
					System.out.println(String
							.format("Using states %s...", text));
					logger.debug("Adding state filtes [{}] from property file",
							text);
				}

				return expressions.toArray(new String[0]);
			}

			private void load(String commonsInstallationId) {
				List<Element> results = findElements(
						getExpressions(commonsInstallationId), 0, 100);

				for (Element element : results) {
					menu.add(createTextMenuItem(element));
					logger.debug(
							"Added publication installation [id: {}, name: {}] to menu",
							element.getId(), element.getName());
				}

				if (results.size() == 0) {
					System.out
							.println("No associated publication installations found.");
					logger.warn("No associated publication installations found");
				}
			}

			public void actionPerformed(TextMenuEvent event) {
				if (event.getIdentifier() == TextMenuEvent.ACTION_ITEM) {
					if (softwareInstallation == null)
						System.out
								.println("No software installation selected.");
					else {
						menu.removeAll();

						String commonsInstallationId = softwareInstallation
								.getId().toString();
						for (Property property : ((SoftwareInstallation) softwareInstallation
								.getConfiguration()).getProperty()) {
							if (property.getName().equals(
									"common.installation.id"))
								commonsInstallationId = property.getValue();
						}
						System.out.println(String
								.format("Finding publications by common installation id [%s].",
										commonsInstallationId));

						if (commonsInstallationId != null)
							load(commonsInstallationId);
						else {
							System.out
									.println("Software installation does not have a common.installation.id property.");
							logger.error(
									"Software installation [id: {}, name: {}] does not have a common.installation.id property.",
									softwareInstallation.getId(),
									softwareInstallation.getName());
						}
					}
				}

				if (event.getIdentifier() == TextMenuEvent.ACTION_EXPRESSION) {
					if (event.getCommand().matches(updateExpression)) {
						if (update) {
							update = false;
							System.out
									.println("Publication installation CIs are not updated after being installed.");
						} else {
							update = true;
							System.out
									.println("Successful installations will automatically be set as Completed.");
						}
					}

					if (event.getCommand().equals("enhet")) {
						if (enhet) {
							enhet = false;
							System.out
									.println("Warning: Enhet property inactive.");
						} else {
							enhet = true;
							System.out
									.println("Warning: Enhet property active until turned off.\n\tPublications are now scp:ed to private directories.");

						}
					}
				}
			}

		});

		menu.setPrompt(" (Install) Enter name or item number > ");
		menu.setPromptExpression(String.format("%s|%s", updateExpression,
				"enhet"));
		menu.setAutoExit(false);

		menu.setHelp("-- Command \"u|update\" toggles updating Publication Installations to Completed on/off.\n"
				+ "\n"
				+ "--Type \"enhet\" to install publications to private enhetstest\n"
				+ "\tdirectories.  Warning: toggling remains active for the application\n"
				+ "\tsession.\n");
		return menu;
	}

	/**
	 * Main menu
	 * 
	 * @return
	 */
	private TextMenu getMainMenu() {
		TextMenu menu = new TextMenu("Main Menu");
		menu.add(getSelectionTextMenu());
		menu.add(getInstallationTextMenu());

		menu.setPrompt(" (Main) Select an action - > ");

		menu.setHelp("-- Commands \"exit|e|quit|q|bye|back|b|..\" exit the application from\n"
				+ "\tthe main menu or return from a submenu.\n"
				+ "\n"
				+ "-- Command \"confirm|c\" toggles confirmation prompts on/off.\n"
				+ "\tfor a particular menu (not all menus or submenus).\n"
				+ "\n"
				+ "-- Command \"all|a\" fires action events for all menu items.\n"
				+ "\tFor example, in the Install submenu all of the listed publications\n"
				+ "\tmay be installed by entering the \"all\" command.\n"
				+ "\n"
				+ "-- Choose a menu index (number) to switch to a submenu.\n");

		return menu;
	}

	/**
	 * Get environment properties
	 * 
	 * @return
	 */
	private Properties getEnvironmentProperties() {
		String name = clientProperties.getProperty("environment.name");

		if (environmentProperties == null) {
			String[] expressions = new String[] { String
					.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element[empty(pojo:toTimeStamp)][pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/context}Environment\"][pojo:name eq \"%s\"]",
							name) };

			Element environment = getElement(expressions);

			/**
			 * Catch if getElement does not return an element then return an
			 * empty Properties
			 */
			if (environment == null) {
				System.out
						.println(String
								.format("WARN: WARN: Either no environment by name [%s] exists or it is not unique (multiple environments by the same name)",
										name));
				return new Properties();
			} else {
				System.out.println(String.format(
						"Adding %d properties from Environment [%s]...",
						environment.getConfiguration().getProperty().size(),
						name));
				environmentProperties = new Properties();
				for (Property property : environment.getConfiguration()
						.getProperty()) {
					logger.debug("Environment property [name: {}, value: {}]",
							property.getName(), property.getValue());
					environmentProperties.put(property.getName(),
							property.getValue());
				}
			}
		}

		return environmentProperties;
	}

	/**
	 * Get organization properties
	 * 
	 * @return
	 */
	private Properties getOrganizationProperties() {
		String name = clientProperties.getProperty("organization.name");

		if (organizationProperties == null) {
			String[] expressions = new String[] { String
					.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element[empty(pojo:toTimeStamp)][pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/context}Organization\"][pojo:name eq \"%s\"]",
							name) };

			Element organization = getElement(expressions);

			/**
			 * Catch if getElement does not return an element then return an
			 * empty Properties
			 */
			if (organization == null) {
				System.out
						.println(String
								.format("WARN: Either no organization by name [%s] exists or it is not unique (multiple organizations by the same name)",
										name));
				return new Properties();
			} else {
				System.out.println(String.format(
						"Adding %d properties from Organization [%s]...",
						organization.getConfiguration().getProperty().size(),
						name));
				organizationProperties = new Properties();
				for (Property property : organization.getConfiguration()
						.getProperty()) {
					logger.debug("Organization property [name: {}, value: {}]",
							property.getName(), property.getValue());
					organizationProperties.put(property.getName(),
							property.getValue());
				}
			}
		}

		return organizationProperties;
	}

	/**
	 * Get software context properties
	 * 
	 * @return
	 */
	private Properties getSoftwareContextProperties(String name) {
		Properties softwareContextProperties = new Properties();

		String[] expressions = new String[] { String
				.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element[empty(pojo:toTimeStamp)][pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/context}Software\"][pojo:name eq \"%s\"]",
						name) };

		Element softwareContext = getElement(expressions);

		/**
		 * Catch if getElement does not return an element then return an empty
		 * Properties
		 */
		if (softwareContext == null) {
			System.out
					.println(String
							.format("WARN: Either no contextual software by name [%s] exists or it is not unique (multiple contextual softwares by the same name)",
									name));
			return new Properties();
		} else {
			System.out.println(String.format(
					"Adding %d properties from contextual Software [%s]...",
					softwareContext.getConfiguration().getProperty().size(),
					name));
			softwareContextProperties = new Properties();
			for (Property property : softwareContext.getConfiguration()
					.getProperty()) {
				logger.debug(
						"Contextual softare property [name: {}, value: {}]",
						property.getName(), property.getValue());
				softwareContextProperties.put(property.getName(),
						property.getValue());
			}
		}

		return softwareContextProperties;
	}

	/**
	 * Get software context properties
	 * 
	 * @return
	 */
	private Properties getPublicationTypeProperties(String name) {
		Properties publicationTypeProperties = new Properties();

		String[] expressions = new String[] { String
				.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element[empty(pojo:toTimeStamp)][pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/context}PublicationType\"][pojo:name eq \"%s\"]",
						name) };

		Element publicationType = getElement(expressions);

		/**
		 * Catch if getElement does not return an element then return an empty
		 * Properties
		 */
		if (publicationType == null) {
			System.out
					.println(String
							.format("WARN: Either no publication type by name [%s] exists or it is not unique (multiple publication types by the same name)",
									name));
			return new Properties();
		} else {
			System.out.println(String.format(
					"Adding %d properties from Publication Type [%s]...",
					publicationType.getConfiguration().getProperty().size(),
					name));
			publicationTypeProperties = new Properties();
			for (Property property : publicationType.getConfiguration()
					.getProperty()) {
				logger.debug("Publication type property [name: {}, value: {}]",
						property.getName(), property.getValue());
				publicationTypeProperties.put(property.getName(),
						property.getValue());
			}
		}

		return publicationTypeProperties;
	}

	/**
	 * Get software context properties
	 * 
	 * @return
	 */
	private Properties getPublicationContextProperties(String name) {
		Properties publicationContextProperties = new Properties();

		String[] expressions = new String[] { String
				.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element[empty(pojo:toTimeStamp)][pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/context}Publication\"][pojo:name eq \"%s\"]",
						name) };

		Element publicationContext = getElement(expressions);

		/**
		 * Catch if getElement does not return an element then return an empty
		 * Properties
		 */
		if (publicationContext == null) {
			System.out
					.println(String
							.format("WARN: Either no contextual publication by name [%s] exists or it is not unique (multiple contextual publications by the same name)",
									name));
			new Properties();
		} else {
			System.out.println(String.format(
					"Adding %d properties from contextual Publication [%s]...",
					publicationContext.getConfiguration().getProperty().size(),
					name));
			publicationContextProperties = new Properties();
			for (Property property : publicationContext.getConfiguration()
					.getProperty()) {
				logger.debug(
						"Contextual publication property [name: {}, value: {}]",
						property.getName(), property.getValue());
				publicationContextProperties.put(property.getName(),
						property.getValue());
			}
		}

		return publicationContextProperties;
	}

	/**
	 * 
	 * @param element
	 */
	private boolean install(Element element) {
		ANTFacade ant = new ANTFacade();
		logger.debug(
				"Initialized ANT facade for publication installation [id: {}, name: {}]",
				element.getId(), element.getName());

		Long publicationId = ((SoftwareInstallation) element.getConfiguration())
				.getSoftware().getId();
		Element publication = getElement(publicationId);

		if (publication == null) {
			System.out
					.println("ERROR: No publication found for the installation CI");
			logger.error(
					"No publication [id: {}] found for the publication installation CI",
					publicationId);
		} else {
			logger.debug("Found publication [id: {}, label: {}]",
					publicationId,
					((Publication) publication.getConfiguration()).getLabel());
			Properties antProperties = new Properties();

			/**
			 * The property order is organization, environment, context
			 * softwares, the actual softare, context publication type.
			 */
			antProperties.putAll(getOrganizationProperties());
			antProperties.putAll(getEnvironmentProperties());

			// Context software properties
			for (Property property : publication.getConfiguration()
					.getProperty())
				if (property.getName().equals("software.context.ancestor")) {
					antProperties.putAll(getSoftwareContextProperties(property
							.getValue()));
					antProperties.put("publication.software.context",
							property.getValue());
				}

			for (Property property : publication.getConfiguration()
					.getProperty())
				if (property.getName().equals("software.context.prefix")) {
					antProperties.putAll(getSoftwareContextProperties(property
							.getValue()));
					antProperties.put("publication.software.context",
							property.getValue());
				}

			// Software properties
			for (Property property : software.getConfiguration().getProperty()) {
				logger.debug("Software property [name: {}, value: {}]",
						property.getName(), property.getValue());
				antProperties.put(property.getName(), property.getValue());
			}
			System.out.println(String.format(
					"Adding %d properties from Software [%s]...", software
							.getConfiguration().getProperty().size(),
					((Software) software.getConfiguration()).getLabel()));

			// Publication type properties
			String type = ((Publication) publication.getConfiguration())
					.getType();
			antProperties.putAll(getPublicationTypeProperties(type));
			antProperties.setProperty("publication.type", type);

			// Context publication properties
			antProperties.putAll(getPublicationContextProperties(publication
					.getName()));

			// Publication properites
			for (Property property : publication.getConfiguration()
					.getProperty()) {
				logger.debug("Publication property [name: {}, value: {}]",
						property.getName(), property.getValue());
				antProperties.put(property.getName(), property.getValue());
			}
			System.out.println(String.format(
					"Adding %d properties from Publication [%s]...",
					publication.getConfiguration().getProperty().size(),
					((Publication) publication.getConfiguration()).getLabel()));

			// Additional properties not contained in other CIs
			String label = ((Publication) publication.getConfiguration())
					.getLabel();
			antProperties.setProperty("publication.name", label);
			antProperties.setProperty("base.publication.name",
					label.replaceAll("\\.tar", ""));
			logger.debug("Set publication.name to {}", label);

			/**
			 * Set enhets flag
			 */
			antProperties.setProperty("enhetstest", enhet ? "true" : "false");

			/**
			 * SCP remote directory
			 */
			SCPLocation otherScpLocation = findScpLocation(label);
			if (otherScpLocation != null) {
				antProperties.setProperty("scp.remotedir",
						otherScpLocation.getDirectory());
				logger.debug("Set scp.remotedir to {}", otherScpLocation);
			}

			ant.setProperties(antProperties);
			ant.parse(antBuildURL);
			ant.setMessageLog(System.out);

			logger.info("Starting ANT build for {} publication...", label);

			try {
				ant.start();
				ant.execute("initialize");
				logger.info("Target initialize complete.");
				ant.execute("scp");
				logger.info("Target scp complete.");
				ant.execute("untar");
				logger.info("Target untar complete.");
				ant.execute("configure");
				logger.info("Target configure complete.");
				ant.finished(null);

				return true;
			} catch (Exception e) {
				System.out.println("***** Installation error *****");
				System.out.println(e.getMessage());
				System.out.println("***** Installation error *****");
				logger.error("Ant failed: {}", e.getMessage());
			}
		}

		return false;
	}

	/**
	 * Constructor
	 * 
	 * @param file
	 */
	public TextBasedClient(File file) {
		if (!file.exists() || !file.isFile())
			throw new InfrastructureException(
					String.format(
							"Property file [%s] either does not exist or is not a file",
							file.getPath()));

		loadClientProperties(file);

		try {
			unmarshaller = CIContext.getCIContext().getJAXBContext()
					.createUnmarshaller();
		} catch (JAXBException e) {
			throw new InfrastructureException(String.format(
					"Unable to create JAXB unmarshaller: %s", e.getMessage()));
		}
	}

	/**
	 * Start client
	 */
	public void execute() {
		TextMenu menu = getMainMenu();
		menu.setVisible();
	}

	/**
	 * Running from the command line
	 * 
	 * @param args
	 */
	@SuppressWarnings("static-access")
	public static void main(String[] args) {
		Options options = new Options();

		Option usage = OptionBuilder.withLongOpt("usage")
				.withDescription("TextBasedClient usage").create();

		Option properties = OptionBuilder.withArgName("property file")
				.hasArgs().withDescription("Load all properties from file")
				.withLongOpt("propertyfile").create("p");

		options.addOption(usage);
		options.addOption(properties);

		CommandLineParser parser = new PosixParser();
		HelpFormatter formatter = new HelpFormatter();

		try {
			File file = null;

			CommandLine line = parser.parse(options, args);

			if (line.hasOption("usage")) {
				formatter.printHelp("TextBasedClient", options);
				System.exit(0);
			}

			if (line.hasOption("p")) {
				file = new File(line.getOptionValue("p"));
			} else {
				throw new InfrastructureException(
						"Option -p (property) is required to load local settings");
			}

			TextBasedClient client = new TextBasedClient(file);
			client.execute();
		} catch (org.apache.commons.cli.ParseException e) {
			System.out
					.println(String
							.format("ParseException caught reading command line arguements: %s",
									e.getMessage()));
		} catch (InfrastructureException e) {
			System.out.println(String.format("Infrastructure exception: %s",
					e.getMessage()));
		} catch (ApplicationException e) {
			System.out.println(String.format("Application exception: %s",
					e.getMessage()));
		}
	}
}